This repository was archived by the owner on Jun 12, 2026. It is now read-only.
EntityTransaction.mergeFind: match by (userId, txid) when present - #152
Merged
sirdeggen merged 1 commit intoApr 24, 2026
Merged
Conversation
Prefer txid when the incoming row has one, falling through to reference only for pre-broadcast rows. `reference` is locally-assigned by whichever storage first ingested the row — it is not a cross-storage identifier — so matching by reference alone duplicates rows whenever two storages independently internalized the same txid under different local references. The fall-through preserves the existing behavior for the createAction → signAction path: when a pre-broadcast row exists on the receiver under some reference and the sender is now delivering the broadcast version with both reference and txid present, the txid lookup misses on the receiver's pre-broadcast row and the reference fall-through finds it so `mergeExisting` stamps the txid on the same row instead of inserting a twin. `mergeExisting` already treats `reference` as never-updated, so post-fix references on each side remain whatever the local storage assigned. Surfaced while debugging duplicate ordinals in yours-wallet after the user wiped local IndexedDB, let `syncAddresses` internalize BRC-29 payments into the empty local store (generating fresh local references), then connected a remote that already held those same txids under its own references. Connecting the remote ran a sync, the old mergeFind by reference missed on both sides, and every ordinal duplicated.
|
shruggr
marked this pull request as draft
April 23, 2026 19:33
shruggr
marked this pull request as ready for review
April 24, 2026 18:17
sirdeggen
approved these changes
Apr 24, 2026
sirdeggen
left a comment
Contributor
There was a problem hiding this comment.
Makes sense to me will give it an AI pass in case I missed something
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes cross-storage transaction deduplication during sync by updating EntityTransaction.mergeFind to match existing transactions by the globally stable (userId, txid) when a txid is available, falling back to (userId, reference) only when needed for pre-broadcast rows.
Changes:
- Update
EntityTransaction.mergeFindto prefer(userId, txid)lookups whenei.txidis present. - Add fallback lookup by
(userId, reference)to support syncing into pre-broadcast rows that don’t yet have a txid.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Problem
EntityTransaction.mergeFindcurrently locates an existing row on the receiver withfindTransactions({ partial: { reference, userId } }). Butreferenceis a locally-assigned random value — each storage that first ingests a txid invents its own reference (viacreateActionorinternalizeAction). It is not a cross-storage identifier.If two storages independently ingest the same bitcoin transaction before sync has had a chance to propagate between them, they each assign a different reference to the same txid. Every subsequent sync between those storages then fails to recognize the rows as the same transaction:
mergeFindby reference misses,mergeNewfires, and a duplicate row is inserted on the receiver.Reproduction observed in yours-wallet
syncAddressesruns against the now-empty local storage and callsinternalizeActionfor every BRC-29 address output found on the indexer, including inbound ordinals that the user had received long before. Each call generates a fresh local reference.setActiverunssyncToWriter,processSyncChunkinvokesmergeFind, the references don't match, every txid duplicates on the remote.The user's ordinals then appear twice in the wallet UI.
Fix
Match by
(userId, txid)when the incoming row has a txid. Fall through to(userId, reference)only for pre-broadcast rows that have not yet learned their txid — this preserves correctness for thecreateAction→ sync-while-unsigned →signAction→ sync-again flow, where the receiver's pre-broadcast row has the reference but no txid yet and needs to be matched on reference somergeExistingcan stamp the txid on it.mergeExistingalready treatsreferenceas never-updated, so the fix leaves each storage's local reference intact; they diverge across storages, as they always did, but sync reconciles by txid and no duplicates are produced.Scope
One function, one file. No schema changes. No other entity's
mergeFindis affected.Verification
Built against 2.1.22. Reproduced the duplication pre-fix in a live wallet, applied the fix, re-ran the same attach-remote flow end-to-end, and confirmed one row per txid on both sides with each retaining its own reference.